iT邦幫忙

2024 iThome 鐵人賽

DAY 5
0
Software Development

那些年,我們一起走過的Go錯系列 第 5

Day05-Go-傳值、傳指標傻傻分不清楚?

  • 分享至 

  • xImage
  •  

前言


寫到第五天覺得這個根本是個考驗我記憶力的比賽和懺悔錄
我記得有次被code review被問說
為什麼要改成不用pointer

我說這個function,不會改變變數
沒必要用pointer

但這樣的說法完整嗎?


本文


你有沒有想過為什麼Golang 預設要用pass by value ?

Golang設計的出發點,為高併發使用和大團隊使用的語言。

預設使用傳值(pass by value)的好處

1.透過預設使用傳值,可以使程式更易於理解,因為所有參數都被視為function內部的拷貝,這減少了副作用的發生。這意味著function內部對參數的修改不會影響到呼叫方的資料,從而減少了不必要的混亂。

(因為語言目的為了高併發,這樣就減少副作用)

2.讓工程師明確地選擇何時傳遞資料的記憶體位址(即指標)。如果你想在function內部修改外部變數的值,可以傳遞變數的指標。這樣可以讓程式碼更具表達性,清楚地表明哪些變數會被function修改,而不僅僅依賴預設行為。
(我覺得這說法非常符合Go的設計理念)

3.在並發環境下,傳值可以防止資料競爭條件的出現。當 Goroutines 在共享資料時,使用傳值可以確保每個 Goroutine 都擁有自己的資料副本,從而避免資料競爭和鎖機制的開銷。

(FYI,Goroutine設計最好是用溝通而非共享)

4.有效控制記憶體的使用。在 Golang 中,當涉及較小資料類型(如整數、boolean等)時,傳值是高效率的,因為這些類型佔用記憶體較少,傳遞它們的副本幾乎沒有效能開銷。當需要傳遞大型資料結構時(如結構體、陣列),可以透過指標來避免複製大量資料,這樣就能控制記憶體和效能的消耗。


回到最初的問題?
雖然在function中,不會改變值
加上參數數據不夠大,不用使用指標來節省記憶體

不過這樣回答就完整了嗎?


前一章講到slice
但從中分割成子slice時
還是會指向本來的slice
因為它的底層是有指標的!!

雖然Golang預設是Pass by value 但其傳遞類型本性不改!

特殊類型的傳遞方式

Slice、Map、Channel、Interface、Function

Slice 的底層結構

type SliceHeader struct {
    Data uintptr // 底層陣列的指標
    Len  int     // 長度
    Cap  int     // 容量
}

傳值時:複製了 SliceHeader,但 Data 指向同一個底層陣列。
影響:函式內部修改元素,外部可見;但重新分配底層陣列後,外部不可見。

Map 的底層結構

type hmap struct {
    // ... 省略其他欄位
    buckets    unsafe.Pointer // 哈希桶的指標
    // ... 省略其他欄位
}

傳值時:複製了 hmap 結構,但 buckets 指向同一個底層哈希表。
影響:函式內部對 map 的增刪改操作,外部均可見。

Channel 的底層結構

type hchan struct {
    // ... 省略其他欄位
    buf   unsafe.Pointer // 緩衝區的指標
    // ... 省略其他欄位
}

傳值時:複製了 hchan 結構,但 buf 指向同一個緩衝區。
影響:多個 Goroutine 可以透過傳遞的 channel 共享資料。

Interface 的底層結構

type iface struct {
    tab  *itab          // 方法表
    data unsafe.Pointer // 資料指標
}

傳值時:複製了介面的結構,但 data 指向同一個具體值。
影響:透過介面修改指向的資料,可能影響原始變數。

含指標的結構

傳值時:複製了結構體,但內部的指標欄位指向原始資料。
影響:修改指標欄位指向的資料,外部可見。


回到最初的問題?

雖然在function中,不會改變值
加上參數數據不夠大,不用使用指標來節省記憶體
而且參數不屬於特殊類型會指向原本的資料

不過這樣回答就完整了嗎?


使用指標處理 SQL 中的 NULL 值

當你不需要區分 NULL 和空字串,只關心column是否有值

type User struct {
    ID    int
    Name  string
    Email *string // nil 表示 NULL,非 nil 表示有值(可能是空字串)
}

func main() {
    // 讀取資料
    var user User
    err := db.QueryRow("SELECT id, name, email FROM users WHERE id = ?", 1).Scan(
        &user.ID, &user.Name, &user.Email)
    if err != nil {
        log.Fatal(err)
    }

    if user.Email != nil {
        fmt.Println("Email:", *user.Email)
    } else {
        fmt.Println("Email is NULL")
    }
}

這個時候也可以用pointer


結語

使用指標的時候
真的要注意最好寫好寫滿測試或者go vet /race指令/staticcheck 等工具做檢查
不然你會常常看到

panic: runtime error: invalid memory address or nil pointer dereference
[signal 0xb code=0x1 addr=0x38 pc=0x26df]

上一篇
DAY04-Go-我說在座的各位都是垃圾--回收要注意的事
下一篇
Day06-Golang框架是什麼?能吃嗎?
系列文
那些年,我們一起走過的Go錯6
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言